Skip to content

001 staff booking flow#39

Merged
nnh53 merged 7 commits into
mainfrom
001-staff-booking-flow
Nov 11, 2025
Merged

001 staff booking flow#39
nnh53 merged 7 commits into
mainfrom
001-staff-booking-flow

Conversation

@nnh53

@nnh53 nnh53 commented Nov 11, 2025

Copy link
Copy Markdown
Member

No description provided.

Copilot AI review requested due to automatic review settings November 11, 2025 23:22
@naming-conventions-bot

Copy link
Copy Markdown

Thank you for following naming conventions! 😻

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 11, 2025

Copy link
Copy Markdown

Deploying ev-rental with  Cloudflare Pages  Cloudflare Pages

Latest commit: 1846931
Status: ✅  Deploy successful!
Preview URL: https://9f7a78a0.ev-rental.pages.dev
Branch Preview URL: https://001-staff-booking-flow.ev-rental.pages.dev

View logs

@nnh53 nnh53 force-pushed the 001-staff-booking-flow branch from 9d95f45 to 809e94f Compare November 11, 2025 23:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a comprehensive staff booking fulfillment workflow allowing staff to process bookings through a mandatory sequential API flow: check-in → rental creation → contract generation → inspection → dual signatures → vehicle handover. The implementation adds a dedicated fulfillment route with a new orchestration layer in core-logic/rental-fulfillment and UI components under features/staff/booking-fulfillment.

Key Changes:

  • New fulfillment orchestration service with signal-based state management enforcing step dependencies
  • Dedicated fulfillment page with step checklist UI, signature capture, and inspection forms
  • Extended staff dashboard with "Tiếp tục xử lý" CTA linking to fulfillment route
  • Analytics service tracking step completion/failures with timeline audit trail

Reviewed Changes

Copilot reviewed 37 out of 38 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/app/features/staff/booking-fulfillment/pages/fulfillment-page/fulfillment-page.ts Main fulfillment page component orchestrating 6-step sequential workflow
src/app/core-logic/rental-fulfillment/fulfillment.service.ts Orchestrator coordinating API calls with error handling and step state management
src/app/core-logic/rental-fulfillment/fulfillment.state.ts Signal-based state store managing step progression and artifacts
src/app/features/staff/staff-dashboard/staff-dashboard.ts Enhanced dashboard with fulfillment CTA and improved change detection
src/app/features/staff/booking-fulfillment/components/inspection-form/inspection-form.ts Reactive form for battery capacity and inspection evidence capture
src/app/features/staff/booking-fulfillment/components/signature-step/signature-step.ts Dual-role signature capture component for renter/staff
src/app/core-logic/bookings/bookings.service.ts Extended with fulfillment summary loader and timeline builder
src/app/layout/layout.html Fixed @for track expressions using unique identifiers
Comments suppressed due to low confidence (1)

src/app/features/staff/booking-fulfillment/pages/fulfillment-page/fulfillment-page.ts:1

  • This method appears in staff-dashboard.ts but the diff shows it at line 609 in fulfillment-page.ts. Verify the correct file location. If duplicated across files, extract to a shared utility since both components need stable card keys for @for tracking.

Comment on lines +355 to +363
// eslint-disable-next-line @typescript-eslint/no-unused-vars
canOpenFulfillment(record: StaffBookingRecord): boolean {
return true;
// return (
// record.verificationStatus === BookingVerificationStatusEnum.Approved ||
// record.status === BookingStatusEnum.Verified ||
// record.status === BookingStatusEnum.RentalCreated
// );
}

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The canOpenFulfillment method bypasses business logic by always returning true, making the unused parameter directive necessary. Either implement the commented validation logic or remove the method entirely if fulfillment access should be unrestricted. The current state creates dead code and misleads readers about intended constraints.

Copilot uses AI. Check for mistakes.
Comment on lines +285 to +290
effect(() => {
this.bookingsService.staffBookings();
this.bookingsService.staffBookingsLoading();
this.bookingsService.staffBookingsError();
queueMicrotask(() => this.cdr.detectChanges());
});

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manual detectChanges() in an effect contradicts Angular 20's zoneless change detection philosophy. Signal reactivity should automatically trigger template updates without explicit ChangeDetectorRef calls. If templates aren't updating, investigate why signals aren't properly connected rather than working around it with manual detection. This pattern undermines the benefits of the zoneless architecture documented in guideline 1000000.

Copilot uses AI. Check for mistakes.
Comment on lines +313 to +318
effect(() => {
this.orchestrator.snapshot();
this.initializationError();
this.routeEntered();
queueMicrotask(() => this.cdr.detectChanges());
});

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to staff-dashboard, this effect manually triggers change detection via ChangeDetectorRef which defeats zoneless signal reactivity. The signals accessed here (snapshot, initializationError, routeEntered) should already notify Angular of changes. Remove this workaround and ensure computed/signal dependencies are properly declared.

Copilot uses AI. Check for mistakes.
Comment on lines +629 to +662
private _resolveTokenUserId(): string | undefined {
const accessToken = this._tokenService.accessToken.token;
const token = this._normalizeString(accessToken);
if (!token) {
return undefined;
}

try {
const payload = this._tokenService.decodeToken(token);
const staffId = this._normalizeString(payload.StaffId);
if (staffId) {
return staffId;
}

const subject = this._normalizeString(payload.sub);
if (subject) {
return subject;
}

const rawPayload = payload as Record<string, unknown>;
const nameIdentifierKey =
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier';
const nameIdentifierClaim = this._normalizeString(
typeof rawPayload[nameIdentifierKey] === 'string'
? (rawPayload[nameIdentifierKey] as string)
: undefined,
);

return nameIdentifierClaim;
} catch {
return undefined;
}
}

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Complex token claim resolution logic with hardcoded XML namespace string should be extracted to TokenService rather than duplicated here. This creates tight coupling to JWT internals and duplicates the fallback chain (StaffIdsub → nameidentifier). Refactor to tokenService.resolveUserId() to centralize token parsing logic.

Suggested change
private _resolveTokenUserId(): string | undefined {
const accessToken = this._tokenService.accessToken.token;
const token = this._normalizeString(accessToken);
if (!token) {
return undefined;
}
try {
const payload = this._tokenService.decodeToken(token);
const staffId = this._normalizeString(payload.StaffId);
if (staffId) {
return staffId;
}
const subject = this._normalizeString(payload.sub);
if (subject) {
return subject;
}
const rawPayload = payload as Record<string, unknown>;
const nameIdentifierKey =
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier';
const nameIdentifierClaim = this._normalizeString(
typeof rawPayload[nameIdentifierKey] === 'string'
? (rawPayload[nameIdentifierKey] as string)
: undefined,
);
return nameIdentifierClaim;
} catch {
return undefined;
}
}
// User ID resolution is now delegated to TokenService.resolveUserId()

Copilot uses AI. Check for mistakes.
<button
type="submit"
class="vehicle-receive__submit"
[disabled]="step.disabled || vehicleReceiveForm.invalid"

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The disabled binding combines orchestrator state (step.disabled) with form validity, but step.disabled already accounts for busy state and prerequisites. This creates redundant validation since the form is disabled when step.disabled is true (via effect at lines 288-293). Simplify to [disabled]=\"step.disabled\" to avoid confusion.

Copilot uses AI. Check for mistakes.
Comment on lines +224 to +241
this._steps.update((steps) => {
const nextSteps: FulfillmentStepState[] = [];
for (const step of steps) {
if (step.step !== stepId) {
nextSteps.push(step);
continue;
}

nextSteps.push({
...step,
...changes,
requires: changes.requires ?? step.requires,
artifact: changes.artifact ?? step.artifact,
error: changes.error,
});
}
return nextSteps;
});

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The loop always rebuilds the entire steps array even when updating a single step. For the 7-step fulfillment workflow this is acceptable, but consider using .map() for clearer intent: steps.map(step => step.step === stepId ? { ...step, ...changes, ... } : step). This makes the transformation more declarative and easier to understand.

Suggested change
this._steps.update((steps) => {
const nextSteps: FulfillmentStepState[] = [];
for (const step of steps) {
if (step.step !== stepId) {
nextSteps.push(step);
continue;
}
nextSteps.push({
...step,
...changes,
requires: changes.requires ?? step.requires,
artifact: changes.artifact ?? step.artifact,
error: changes.error,
});
}
return nextSteps;
});
this._steps.update((steps) =>
steps.map((step) =>
step.step === stepId
? {
...step,
...changes,
requires: changes.requires ?? step.requires,
artifact: changes.artifact ?? step.artifact,
error: changes.error,
}
: step,
)
);

Copilot uses AI. Check for mistakes.
Comment on lines +311 to +318
<button
type="button"
class="details-panel__action"
(click)="openFulfillment(detail.record)"
[disabled]="!canOpenFulfillment(detail.record)"
>
Tiếp tục xử lý
</button>

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The button lacks aria-label or aria-describedby to explain what 'Tiếp tục xử lý' means in context. Screen reader users won't know this continues processing for the specific booking. Add [attr.aria-label]=\"'Tiếp tục xử lý booking ' + detail.record.bookingId\" to provide context.

Copilot uses AI. Check for mistakes.
@nnh53 nnh53 force-pushed the 001-staff-booking-flow branch from 809e94f to 1846931 Compare November 11, 2025 23:26
@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

private _buildFulfillmentTimeline(record: StaffBookingRecord): FulfillmentTimelineEvent[] {
const now = new Date().toISOString();
const events: FulfillmentTimelineEvent[] = [];
if (record.verificationStatus === BookingVerificationStatusEnum.Approved) {
const occurredAt =
this._normalizeString(record.verifiedAt) ??
this._normalizeString(record.bookingCreatedAt) ??
now;
events.push({
step: 'checkin',
title: 'Đặt xe đã được duyệt',
description: 'Nhân viên đã xác nhận đặt xe và chuyển sang chuẩn bị thuê.',
actor: 'staff',
occurredAt,
metadata: this._buildMetadata({ staffId: record.verifiedByStaffId }),
});
}
const rentalId = this._normalizeString(record.rental?.rentalId);
if (rentalId) {
const rentalStartAt =
this._normalizeString(record.rental?.startTime) ??
this._normalizeString(record.rental?.booking?.startDate) ??
now;
events.push({
step: 'create-rental',
title: 'Đơn thuê đã được tạo',
description: 'Đơn thuê được khởi tạo sau khi booking được phê duyệt.',
actor: 'staff',
occurredAt: rentalStartAt,
metadata: this._buildMetadata({ rentalId }),
});
const validContracts = (record.rental?.contracts ?? []).reduce<ContractDto[]>(
(accumulator, contract) => {
if (contract?.contractId) {
accumulator.push(contract);
}
return accumulator;
},
[],
);
if (validContracts.length > 0) {
const sortedContracts = [...validContracts].sort((first, second) => {
const firstTime = Date.parse(first.issuedAt ?? '');
const secondTime = Date.parse(second.issuedAt ?? '');
return firstTime - secondTime;
});
const latestContract = sortedContracts[sortedContracts.length - 1];
const occurredAt = this._normalizeString(latestContract.issuedAt) ?? rentalStartAt;
events.push({
step: 'create-contract',
title: 'Hợp đồng đã được phát hành',
description: 'Hợp đồng điện tử gắn với đơn thuê đã sẵn sàng cho chữ ký.',
actor: 'staff',
occurredAt,
metadata: this._buildMetadata({ contractId: latestContract.contractId }),
});
}
}
return events.sort(
(first, second) => Date.parse(first.occurredAt) - Date.parse(second.occurredAt),
);

P1 Badge Persist post-contract steps in fulfillment timeline

The fulfillment store restores step state after a refresh by scanning timeline events (see _applySummary), but _buildFulfillmentTimeline only ever emits events for checkin, create-rental, and create-contract. Inspection, renter/staff signatures and vehicle handoff never get emitted even when that data exists on record.rental. As soon as the page refreshes or refresh() is called after completing those steps, the timeline returned by the service contains no matching events, so the UI resets those steps back to “pending” and allows duplicate actions. The timeline should also include inspection/signature/vehicle-receive events so completed work survives a reload.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@nnh53 nnh53 merged commit df1ee7c into main Nov 11, 2025
5 checks passed
@nnh53 nnh53 deleted the 001-staff-booking-flow branch November 11, 2025 23:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants